package edu.umd.ion.wavemeter.server;

import edu.umd.ion.wavemeter.service.ChannelEvent;
import edu.umd.ion.wavemeter.service.ChannelRoutingInfo;
import edu.umd.ion.wavemeter.service.ServerParameters;
import edu.umd.ion.wavemeter.service.WavemeterData;
import edu.umd.ion.wavemeter.service.WavemeterException;
import edu.umd.ion.wavemeter.service.WavemeterParameters;
import edu.umd.ion.wavemeter.service.WavemeterSetException;

import java.util.Date;
import java.util.Iterator;
import java.util.TreeMap;
import java.util.SortedMap;
import java.util.Vector;
import java.util.Map.Entry;

public class DynamicChannelManager implements ChannelManager {
	private static int UniqueWeight = 256;
	
	private RawChannelData[] RawChannel;
	private int NumVirtChannels = 0;
	private int NumLockedChannels = 0;
	private int NumActiveChannels = 0;
	private final int NumRawChannels = WavemeterConfiguration.NumRawChannels();
	private final int MaxVirtChannels = WavemeterConfiguration.NumVirtChannels();
	private Integer[] ActiveVirtChannels = new Integer[MaxVirtChannels];
	private TreeMap<Integer,Integer> VirtChannelMap = 
		new TreeMap<Integer,Integer>();
	private PollingThreadInterface WavemeterDevice;
	private ExposureControl MyExposureControl;
	private WavemeterData Data;
	private boolean CalibrationLockEngaged;

	private class VirtualChannelData {
		RunnableOne<Integer> Lock, Unlock;
		boolean LockedChannel;
		long TimeoutMillis;
	}
	
	private VirtualChannelData[] VirtualData;
	
	/**
	 * Communication with FiberSwitcher 
	 */
	private FiberSwitcherComm Comm;
	/**
	 *  A channel is switched off after it is underexposed for more than xxx in msecs
	 */
	private long UnderexposedTimeout = 30000;

	public DynamicChannelManager(PollingThreadInterface Device, WavemeterData data, ExposureControl c) {
		WavemeterDevice = Device;
		Data = data;
		MyExposureControl = c;
		RawChannel = new RawChannelData[NumRawChannels];
		for (int i=0; i<NumRawChannels; ++i) {
			RawChannel[i] = new RawChannelData();
		}
		VirtualData = new VirtualChannelData[MaxVirtChannels];
		for (int i=0; i<MaxVirtChannels; ++i) {
			VirtualChannelData v = new VirtualChannelData();
			v.TimeoutMillis = Long.MAX_VALUE;
			VirtualData[i] = v;
		}
		try {
			Comm = new FiberSwitcherComm();
			Comm.open(WavemeterParameters.FiberSwitcherPortName);
			System.out.println("FiberSwitcherPort '"+WavemeterParameters.FiberSwitcherPortName+"' opened.");
		} catch (Exception ex) {
			System.out.println("Opening of FiberSwitcherPort '"+WavemeterParameters.FiberSwitcherPortName+"' failed.");
		}
		for (int i=0; i<NumRawChannels; ++i) {
			pushRouting(i,i);
		}
		CalibrationLockEngaged = false;
	}

	public void cleanup() {
		try {
			if (Comm!=null) Comm.close();
		}
		catch (Exception ex) {			
		}
	}
	
	public void pushInterlock(int virtchannel, boolean engage) {
		byte[] message = { 0x4F, 0x30, 0x30, 0x30, 0x30, 0x0d, 0x0a };
		if (engage) {
				message[1+virtchannel/4] |= 1 << (virtchannel%4);
		}
		System.out.println("pushing Interlock "+message[1]%16+" "+message[2]%16+" "+message[3]%16+" "+message[4]%16+" ");
		try {
			if (Comm!=null) {
				Comm.write( message );
				Comm.flush();
			}
		} catch (Exception ex) {
			System.out.println("Write to FiberSwitch failed: "+ex.getMessage());
		}
	}

	
	/**
	 * Push a new routing table entry to the FPGA of the switcher
	 * @param rawchannel
	 */
	private void pushRouting(int rawchannel, int virtchannel) {
		if (!CalibrationLockEngaged && RawChannel[rawchannel].activeVirtChannel() != virtchannel) {
			byte[] message = { 0x73, 0x31, 0x30, 0x0d, 0x0a };
			//System.out.println("pushing routing "+virtchannel+"->"+rawchannel);
			try {
				message[1] += rawchannel;
				message[2] += virtchannel;
				if (Comm!=null) {
					Comm.write( message );
					Comm.flush();
				}
			} catch (Exception ex) {
				System.out.println("Write to FiberSwitch failed: "+ex.getMessage());
			}
			RawChannel[rawchannel].setActiveVirtChannel(virtchannel); 
		}
	}
	
	private void setRawChannelActive(int rawchannel, boolean active, boolean force ) throws WavemeterException {
		if (!force && RawChannel[rawchannel].ChannelActive == active) return;
		int result;
		if (active) {
			int virtchannel = ActiveVirtChannels[ RawChannel[rawchannel].currentChannelIndex() ];
			pushRouting(rawchannel, virtchannel);
			MyExposureControl.pushExposure(rawchannel, virtchannel);
		}
		if (active && NumActiveChannels==0) {
			WavemeterDevice.setEnable(true);
			result = WavemeterLibrary.SetSwitcherSignal(rawchannel+1,  1, 0);
			if (result!=0) throw new WavemeterSetException("SetSwitcherSignal",result);
			for (int i=0; i<NumRawChannels; ++i)
				if (i!=rawchannel) WavemeterLibrary.SetSwitcherSignal(i+1,  0, 0);
		} else if (!active && NumActiveChannels<=1){
			WavemeterDevice.setEnable(false);
		} else {
			result = WavemeterLibrary.SetSwitcherSignal(rawchannel+1,  active?1:0, 0);
			if (result!=0) throw new WavemeterSetException("SetSwitcherSignal",result);
		}
		NumActiveChannels = Math.max( (active?1:-1)+NumActiveChannels , 0 );
		RawChannel[rawchannel].ChannelActive = active;	
	}
	
	private int calculateWeight(int channel) {
		int exposure = Math.max(Data.ChannelData[channel].Exposure1,Data.ChannelData[channel].Exposure2);
		int maxexposure = MyExposureControl.getMaxExposure();
		int locked = Data.ChannelData[channel].LockEnabled ? 2 : 1;
		return  UniqueWeight+locked*(maxexposure-exposure);
	}
	
	private TreeMap<Integer,Integer> weightedActiveChannels() {
		TreeMap<Integer,Integer> WeightedMap = new TreeMap<Integer, Integer>();
		VirtChannelMap.entrySet();
		for (Iterator<Entry<Integer,Integer>> it=VirtChannelMap.entrySet().iterator(); it.hasNext(); ) {
			Entry<Integer,Integer> Current = it.next( );
			int weight = calculateWeight(Current.getValue());
			while (WeightedMap.containsKey(weight)) { ++weight; }
		    WeightedMap.put( weight, Current.getValue());
		}
		return WeightedMap;
	}
	
	private void reroute() throws WavemeterException {
		TreeMap<Integer,Integer> MyWeightedChannelMap = weightedActiveChannels();
		NumVirtChannels = MyWeightedChannelMap.size();
		ActiveVirtChannels = MyWeightedChannelMap.values().toArray(ActiveVirtChannels);
		SortedMap<Integer,Integer> HeadMap = MyWeightedChannelMap.headMap(UniqueWeight-1);
		NumLockedChannels = HeadMap.size();
		int NumUnlockedChannels = NumVirtChannels-NumLockedChannels;
		int MinChannelsToServe = NumUnlockedChannels/(NumRawChannels-NumLockedChannels);
		int BusyChannelNumber = NumUnlockedChannels%(NumRawChannels-NumLockedChannels);
		
		int NumUnlockedChannelsServed = 0;
		int CurrentLockedIndex = 0;
		int NumUnlockedRawChannelsServed = 0;
		int CurrentUnlockedIndex = NumLockedChannels;
		System.out.println((new Date()).toString()+" reroute VirtChannels:"+NumVirtChannels+
				" LockedChannels:"+NumLockedChannels);
		for (int i=0; i<NumRawChannels; ++i) {
			if (HeadMap.containsKey(i)) {
				RawChannel[i].setPointers(CurrentLockedIndex, ++CurrentLockedIndex, true);
				setRawChannelActive(i,true,false);
			} else if ( NumUnlockedChannelsServed<NumUnlockedChannels ){
				int ChannelsToServe = NumUnlockedRawChannelsServed<BusyChannelNumber ? MinChannelsToServe+1 : MinChannelsToServe;
				RawChannel[i].setPointers(CurrentUnlockedIndex, CurrentUnlockedIndex+=ChannelsToServe, false);
				setRawChannelActive(i,true,false);				
				NumUnlockedChannelsServed += ChannelsToServe;
				++NumUnlockedRawChannelsServed;
			} else {
				RawChannel[i].setPointers(0,0,false);
				if (RawChannel[i].ChannelActive) {
					setRawChannelActive(i,true,false);
				}
			}				
		}
	}
	
	public void setRawChannelState(int rawchannel, boolean active) {
		if (RawChannel[rawchannel].ChannelActive != active) {
			RawChannel[rawchannel].ChannelActive = active;
			NumActiveChannels += active ? 1 : -1;
		}
	}
	
	public void setChannelActive(int virtchannel, boolean active) throws WavemeterException {
		if (active) {
			VirtualData[virtchannel].TimeoutMillis = Long.MAX_VALUE;
			if (VirtChannelMap.put(virtchannel+UniqueWeight, virtchannel) == null) {
				reroute();
			}
		} else {
			if ( VirtChannelMap.remove(virtchannel) != null ||
					VirtChannelMap.remove(virtchannel+UniqueWeight) != null ) {
				if (VirtualData[virtchannel].Unlock!=null) {
					VirtualData[virtchannel].Unlock.run(virtchannel);
					VirtualData[virtchannel].Unlock=null;					
				}
				reroute();
			} 
		}
		Data.ChannelData[virtchannel].active = active;
		WavemeterDevice.notifyClients();
	}
	
	public void setChannelLocked(int virtchannel, boolean locked) throws WavemeterException {
		if (locked) {
			if (virtchannel<NumRawChannels) {
				if (!VirtChannelMap.containsKey(virtchannel)) {
					VirtChannelMap.remove(virtchannel+UniqueWeight);
					VirtChannelMap.put(virtchannel, virtchannel);
					reroute();
				}
			}
		} else {
			if ( VirtChannelMap.remove(virtchannel)!=null ) {
				VirtChannelMap.put(virtchannel+UniqueWeight, virtchannel);
				reroute();
			}
		}
	}

	// Query Functions
	public int activeVirtChannel(int rawchannel) {
		return RawChannel[rawchannel].activeVirtChannel();
	}
	
	public int lastVirtChannel(int rawchannel) {
		return RawChannel[rawchannel].LastVirtChannel;
	}
	
	public int scheduleChannel(int rawchannel) throws WavemeterException {
		try {
				int NextIndex = RawChannel[rawchannel].nextChannelIndex();
				int Next = ActiveVirtChannels[NextIndex];
				MyExposureControl.pushExposure(rawchannel, Next);
				pushRouting(rawchannel, Next);
				if ( VirtualData[rawchannel].Lock != null) {
					VirtualData[rawchannel].Lock.run(rawchannel);
					VirtualData[rawchannel].Lock = null;
				}
				return Next;
		}
		catch (ChannelNothingToDo ex) {
			System.out.println("Exception: Nothing to do, Switching off Channel "+rawchannel);
			setRawChannelActive(rawchannel,false,false);
			throw ex;
		}
	}
	
	public int activeChannels() {
		return NumVirtChannels;
	}

	@Override
	public void registerRawChannelCallback(int rawchannel,
			RunnableOne<Integer> lock, RunnableOne<Integer> unlock) {
		VirtualData[rawchannel].Lock = lock; 
		VirtualData[rawchannel].Unlock = unlock;
	}


	@Override
	public void obtainedResult(int virtchannel, boolean good) throws WavemeterException {
		if (good) {
			VirtualData[virtchannel].TimeoutMillis = Long.MAX_VALUE; 
			return;
		}
		if (VirtualData[virtchannel].TimeoutMillis==Long.MAX_VALUE) {
			VirtualData[virtchannel].TimeoutMillis = System.currentTimeMillis()+UnderexposedTimeout;
		}		
		if (System.currentTimeMillis()>VirtualData[virtchannel].TimeoutMillis) {
			System.out.println(System.currentTimeMillis()+" > "+VirtualData[virtchannel].TimeoutMillis);
			setChannelActive(virtchannel,false);
		}
	}

	@Override
	public int calibrationChannelLock(int virtchannel) throws WavemeterSetException {
		WavemeterDevice.setEnable(false);
		pushRouting(0,virtchannel);
		CalibrationLockEngaged = true;
		WavemeterLibrary.SetSwitcherChannel(0+1);
		return 0;
	}

	@Override
	public void calibrationChannelUnlock() throws WavemeterSetException {
		CalibrationLockEngaged = false;
		WavemeterDevice.setEnable(true);		
	}

	@Override
	public Vector<ChannelRoutingInfo> channelRouting() {
		Vector<ChannelRoutingInfo> Routing = new Vector<ChannelRoutingInfo>();
		for (int i=0; i<NumRawChannels; ++i ) {
			for (int j=RawChannel[i].idx_begin; j!=RawChannel[i].idx_end; ++j) {
				ChannelRoutingInfo ChannelInfo = new ChannelRoutingInfo();
				ChannelInfo.RawChannel = i;
				ChannelInfo.VirtChannel = ActiveVirtChannels[j];
				ChannelInfo.Exposure = Math.max(Data.ChannelData[ChannelInfo.VirtChannel].Exposure1, 
						Data.ChannelData[ChannelInfo.VirtChannel].Exposure2);
				ChannelInfo.LockEngaged = RawChannel[i].LockedChannel;
				Routing.add(ChannelInfo);
			}
		}
		return Routing;
	}

	@Override
	public void setParameters(ServerParameters param) {
		MyExposureControl.setParameters(param);
		param.UnderexposedTimeout = Math.max(1000, param.UnderexposedTimeout);
		UnderexposedTimeout = param.UnderexposedTimeout;
	}

	@Override
	public ServerParameters getParameters() {
		ServerParameters P = new ServerParameters();
		P.UnderexposedTimeout = UnderexposedTimeout;
		return P;
	}

	@Override
	public void raiseChannelEvent(ChannelEvent E) {
		// Nothing to do right now
		
	}

}
